﻿using HtmlAgilityPack;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
using System.Xml.Linq;

namespace MakeWeekConcerts.Models
{
    public class Concerts
    {
        public string ConcertUrl { get; set; }

        public DateTime LastRefresh { get; set; }

        public List<ConcertData> ConcertList { get; set; }
    }

    public class ZVentsRssParser
    {
        private static readonly XNamespace GeoNS = "http://www.w3.org/2003/01/geo/wgs84_pos#";
        private static readonly XNamespace DCNS = "http://purl.org/dc/elements/1.1/";
        private static readonly XNamespace XCalNS = "urn:ietf:params:xml:ns:xcal";
        private static readonly XNamespace GeoRssNS = "http://www.georss.org/georss";

        public static Concerts ConvertEventsToConcerts(EventListings events)
        {
            var concerts = new Concerts();
            concerts.LastRefresh = events.LastRefresh;
            concerts.ConcertUrl = events.SourceUrl;
            concerts.ConcertList = events.Events.Select(x => ConvertEventToConcertData(x)).ToList();
            return concerts;
        }

        public static ConcertData ConvertEventToConcertData(Event evt)
        {
            var concert = new ConcertData();
            
            return concert;
        }

        public static EventListings ParseEventFeed(XElement feed)
        {
            var listings = new EventListings();
            listings.Title = Stringify(feed, null, "title");
            listings.LastRefresh = DateTime.UtcNow;
            listings.Events.AddRange(feed.Descendants("item").Select(x => ParseEvent(x)));
            return listings;
        }

        /// <summary>
        ///  <xCal:dtstart>2014-02-13 20:00:00 +0000</xCal:dtstart>
        ///  <xCal:dtend></xCal:dtend>
        ///  <xCal:location>http://www.zvents.com/seattle_wa/venues/show/641879-showbox-sodo</xCal:location>
        ///  <xCal:x-calconnect-venue>...</xCal:x-calconnect-venue>
        /// </summary>
        public static Event ParseEvent(XElement element)
        {
            var evt = new Event();
            evt.Title = Stringify(element, null, "title");
            evt.Description = Stringify(element, null, "description");
            evt.Start = Dateify(element, XCalNS, "dtstart");
            evt.End = Dateify(element, XCalNS, "dtend");
            evt.Venue = ParseVenue(element.Descendants(XCalNS + "x-calconnect-venue").Single());
            return evt;
        }

        /// <summary>
        ///  <xCal:x-calconnect-venue>
        ///   <xCal:x-calconnect-venue-id>641879</xCal:x-calconnect-venue-id>
        ///   <xCal:adr>...</xCal:adr>
        ///   <xCal:url>http://www.showboxpresents.com</xCal:url>
        ///   <xCal:x-calconnect-tel>(206) 628-3151</xCal:x-calconnect-tel>
        ///  </xCal:x-calconnect-venue>
        /// </summary>
        /// <remarks>
        /// venue-name is in the adr block for some reason, I move it up.
        /// </remarks>
        public static Venue ParseVenue(XElement element)
        {
            var venue = new Venue();
            Func<string, string> GetVal = s => Stringify(element, XCalNS, s);
            venue.Id = GetVal("x-calconnect-venue-id");
            venue.Name = GetVal("x-calconnect-venue-name");
            venue.Url = GetVal("url");
            venue.Telephone = GetVal("x-calconnect-tel");
            venue.Location = ParseEventLoc(element.Descendants(XCalNS + "adr").Single());
            return venue;
        }

        /// <summary>
        /// <xCal:adr>
        ///   <xCal:x-calconnect-venue-name>Showbox SoDo</xCal:x-calconnect-venue-name>
        ///   <xCal:x-calconnect-street>1700 First Ave. South</xCal:x-calconnect-street>
        ///   <xCal:x-calconnect-city>Seattle</xCal:x-calconnect-city>
        ///   <xCal:x-calconnect-region>WA</xCal:x-calconnect-region>
        ///   <xCal:x-calconnect-postalcode>98134</xCal:x-calconnect-postalcode>
        ///   <xCal:x-calconnect-country>United States</xCal:x-calconnect-country>
        /// </xCal:adr>
        /// </summary>
        public static Location ParseEventLoc(XElement element)
        {
            var loc = new Location();
            Func<string, string> GetVal = s => Stringify(element, XCalNS, "x-calconnect-" + s);
            loc.Street = GetVal("street");
            loc.City = GetVal("city");
            loc.Region = GetVal("region");
            loc.PostalCode = GetVal("postalcode");
            loc.Country = GetVal("country");
            return loc;
        }

        private static string Stringify(XElement ancestor, XNamespace ns, string eltName)
        {
            XElement elt;
            if (ns != null)
                elt = ancestor.Descendants(ns + eltName).FirstOrDefault();
            else
                elt = ancestor.Descendants(eltName).FirstOrDefault();
            if (elt == null)
                return string.Empty;
            return elt.Value;
        }

        private static DateTime? Dateify(XElement ancestor, XNamespace ns, string eltName)
        {
            var dateStr = Stringify(ancestor, ns, eltName);
            DateTime? date = null;
            if (!string.IsNullOrEmpty(dateStr))
                date = DateTime.Parse(dateStr);
            return date;
        }
    }

    public class EventListings
    {
        public EventListings()
        {
            this.Events = new List<Event>();
        }

        public string Title { get; set; }

        public string SourceUrl { get; set; }

        public DateTime LastRefresh { get; set; }

        public List<Event> Events { get; set; }
    }

    public class Event
    {
        public string Title { get; set; }

        public string Description { get; set; }

        public DateTime? Start { get; set; }

        public DateTime? End { get; set; }

        public Venue Venue { get; set; }
    }

    public class Venue
    {
        public string Id { get; set; }

        public string Name { get; set; }

        public Location Location { get; set; }

        public string Url { get; set; }

        public string Telephone { get; set; }
    }

    public class Location
    {
        public string Street { get; set; }

        public string City { get; set; }

        public string Region { get; set; }

        public string PostalCode { get; set; }

        public string Country { get; set; }
    }

    public class ConcertData
    {
        public ConcertData()
        {
            this.Artists = new List<string>();
            this.EventTimes = new List<DateTime>();
        }

        [JsonIgnore]
        public HtmlNode Node { get; set; }

        public string AsString { get; set; }

        public string Name { get; set; }

        public List<string> Artists { get; set; }

        public string Details { get; set; }

        public Uri EventUri { get; set; }

        public string EventTimeStr { get; set; }

        public List<DateTime> EventTimes { get; set; }

        public Uri EventLocationUri { get; set; }

        public string EventLocationName { get; set; }

        public string EventLocationDetails { get; set; }

        [JsonIgnore]
        public string Id { get; set; }
    }

    public class StrangerConcertParser
    {
        private const string StrangerEventsUrl = "http://www.thestranger.com/gyrobase/EventSearch?eventSection=3208279&page={0}";
        private static readonly Regex EventTimeRegex = new Regex(@"\S+ (?<Month>Jan(uary)?|Feb(ruary)?|Mar(ch)?|Apr(il)?|May|Jun(e)?|Jul(y)?|Aug(ust)?|Sep(tember)?|Oct(ober)?|Nov(ember)?|Dec(ember)?) (?<Day>[0-9]+)( at (?<Time>[0-9]+(:[0-9]+)?) (?<AMPM>am|pm))?\.");

        public static DateTime? ToDate(Match timeRegexMatch)
        {
            var year = DateTime.UtcNow.Year.ToString();
            var month = timeRegexMatch.Groups["Month"].Value;
            var day = timeRegexMatch.Groups["Day"].Value;
            var dateStr = string.Empty;
            if (!string.IsNullOrEmpty(timeRegexMatch.Groups["Time"].Value))
            {
                var time = timeRegexMatch.Groups["Time"].Value;
                var ampm = timeRegexMatch.Groups["AMPM"].Value;
                dateStr = string.Format("{0}/{1}/{2} {3} {4}", year, month, day, time, ampm);
            }
            else
                dateStr = string.Format("{0}/{1}/{2}", year, month, day);

            DateTime result;
            if (DateTime.TryParse(dateStr, out result))
                return result;

            Trace.TraceInformation("Failed to parse {0}", dateStr);
            return null;
        }

        public async Task<Concerts> FetchAndParseConcertsAsync()
        {
            // Load stranger page
            var client = new HttpClient();
            var data = new Concerts() { ConcertUrl = StrangerEventsUrl, ConcertList = new List<ConcertData>() };

            var curPage = 1;
            bool hadData = false;
            do
            {
                hadData = false;
                var curUrl = string.Format(StrangerEventsUrl, curPage);
                Trace.TraceInformation("Fetching page {0} from {1}", curPage, curUrl);
                var result = await client.GetAsync(curUrl);
                if (result.StatusCode == System.Net.HttpStatusCode.OK)
                {
                    var content = await result.Content.ReadAsStringAsync();
                    var doc = new HtmlDocument();
                    doc.LoadHtml(content);
                    var eventNodes = doc.DocumentNode.SelectNodes("//div[@class='EventListing clearfix']");
                    if (eventNodes != null)
                    {
                        foreach (var node in eventNodes)
                        {
                            hadData = true;
                            var curConcert = new ConcertData() { Node = node, AsString = node.WriteTo() };
                            var listing = node.SelectSingleNode("div[@class='listing']");
                            if (listing != null)
                            {
                                var eventRef = listing.SelectSingleNode(".//a[@class='search-results-title']");
                                if (eventRef != null)
                                {
                                    curConcert.EventUri = new System.Uri(eventRef.Attributes["href"].Value);
                                    curConcert.Name = eventRef.InnerText;
                                    curConcert.Artists = eventRef.InnerText.Split(',').Select(x => x.Trim()).Distinct().ToList();
                                }
                                var dateTxt = "";
                                foreach (var txt in listing.ChildNodes.Where(x => x.NodeType == HtmlNodeType.Text))
                                {
                                    dateTxt += txt.InnerText;
                                }
                                curConcert.EventTimeStr = dateTxt.Trim();
                                var match = EventTimeRegex.Match(curConcert.EventTimeStr);
                                if (match.Success)
                                {
                                    var date = ToDate(match);
                                    if (date.HasValue)
                                        curConcert.EventTimes.Add(date.Value);
                                }
                                var details = listing.SelectSingleNode(".//p[@class='descripTxt']");
                                if (details != null)
                                    curConcert.Details = details.InnerText;
                            }
                            var listingLoc = node.SelectSingleNode("div[@class='listingLocation']");
                            if (listingLoc != null)
                            {
                                var listingRef = listingLoc.SelectSingleNode("a[@href]");
                                if (listingRef != null)
                                {
                                    curConcert.EventLocationUri = new System.Uri(listingRef.Attributes["href"].Value);
                                    curConcert.EventLocationName = listingRef.InnerText;
                                }
                                curConcert.EventLocationDetails = listingRef.InnerText;
                            }
                            data.ConcertList.Add(curConcert);
                        }
                    }
                }
                curPage++;
            } while (hadData);

            data.LastRefresh = DateTime.UtcNow;
            return data;
        }
    }

}